Elasticsearch篇之数据建模
数据建模
- 英文名为 Data Modeling, 为创建数据模型的过程
- 数据模型 (Data Model)
- 对现实世界进行抽象描述的一种工具和方法
- 通过抽象的实体及实体之间的联系的形式去描述业务规则, 从而实现对现实世界的映射
数据建模的过程
- 概念模型
- 确定系统的核心需求和范围边界, 设计实体和实体间的关系
- 逻辑模型
- 进一步梳理业务需求, 确定每个实体的属性, 关系和约束等
- 物理模型
- 结合具体的数据库产品, 在满足业务读写性能等需求的前提下确定最终的定义
- MySQL, MongoDB, Elasticsearch等
- 第三范式
数据建模的意义
重视数据建模
- 牵一发而动全身
ES中的数据建模
- ES是基于Lucene以倒排索引为基础实现的储存体系, 不完全遵循关系型数据库中的范式约定
Mapping字段的相关设置
- enabled
- true
- false
- 仅存储, 不做搜索或聚合分析
- 节省磁盘空间
- 示例: 在web应用存储cookie, session等字段
- index
- true
- false
- 是否构建倒排索引
- index_options
- docs | freqs | positions | offsets
- 存储倒排索引的哪些信息
- norms
- true
- false
- 是否存储归一化相关参数, 如果字段仅用于过滤和聚合分析(不进行相关性算分), 可关闭
- doc_values
- true
- 用于排序和聚合分析
- false
- 仅用于搜索, 可关闭
- true
- field_data
- false
- true
- 是否为text类型启用field_data, 实现排序和聚合分析
- store
- true
- false
- 不存储该字段值
- coerce
- true
- 开启自动数据类型转换功能, 比如字符串转为数字, 浮点型转为整型
- false
- true
- mutifields多字段
- 灵活使用多字段特性来解决多样的业务需求
- dynamic
- true | false | strict
- 控制mapping自动更新
- date_detection
- true | false
- 是否自动识别日期类型
Mapping字段属性的设定流程
是何种类型?
- 字符串类型
- 需要分词则设定为text类型, 否则设置为keyword类型
- 枚举类型
- 基于性能考虑将其设置为keyword类型, 即便该数据为整型
- 数值类型
- 尽量选择贴近的类型, 比如byte即可表示所有数值时, 即选用byte, 不要用long
- 其他类型
- 比如布尔类型, 日期, 地理位置数据等
是否需要检索?
- 完全不需要检索, 排序, 聚合分析的字段
- enabled设置为false
- 不需要检索的字段
- index设置为false
- 需要检索的字段, 可以通过如下配置设定需要的存储粒度
- index_options结合需要设定
- norms不需要归一化数据时关闭即可
是否需要排序和聚合分析?
- 不需要排序或者聚合分析功能
- doc_values 设定为false
- fielddata 设定为false
是否需要另行存储
- 是否需要专门存储当前字段的数据?
- store 设定为true, 即可存储该字段的原始内容 (与_source中的不相关)
- 一般结合_source的enabled设定为false时使用
实例
博客文章blog_index
- 标题 title
- 发布日期 publish_date
- 作者 author
- 摘要 abstract
- 网络地址 url
Mapping设置
- blog_index的mapping设置如下
索引主体变更
- 当向blog_index添加一个
内容 content
时, 由于该字段内容有可能数据量非常大, 故将会带来以下问题- 在进行其他字段搜索时, 由于显示_source字段时把所有原始数据显示出来, 显示的content字段内容将会带来很大的性能问题
Mapping设置 (改进)
- blog_index的mapping设置如下
注意: 由于_source设置为了false, 故在设置url时不能简单的设置enabled为false
检索改进
演示
# request
# 设置blog_index的mapping (版本1)
PUT blog_index
{
"mappings": {
"doc":{
"properties": {
"title":{
"type": "text",
"fields": {
"keyword":{
"type":"keyword",
"ignore_above": 100
}
}
},
"publish_date":{
"type":"date"
},
"author":{
"type":"keyword",
"ignore_above": 100
},
"abstract":{
"type": "text"
},
"url":{
"enabled":false
},
"content":{
"type":"text"
}
}
}
}
}
# 为blog_index索引添加文档
POST blog_index/doc/1
{
"title": "blog title",
"content": "blog content"
}
# 检索blog_index索引的数据
GET blog_index/_search
# response
# 可见由于_source字段把所有属性及其内容全部展示, 假设content数据内容非常大将及其耗费性能 (如果进行网络通信, 也将及其耗费带宽)
{
"took": 56,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "blog_index",
"_type": "doc",
"_id": "1",
"_score": 1,
"_source": {
"title": "blog title",
"content": "blog content"
}
}
]
}
}
# 或许可以利用如下搜索
# 但此搜索的底层原理只是在已经检索出数据后进行简单的过滤, 在检索context时也避免不了性能问题
GET blog_index/_search?_source=title
# response
{
"took": 64,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "blog_index",
"_type": "doc",
"_id": "1",
"_score": 1,
"_source": {
"title": "blog title"
}
}
]
}
}
# 结合以上产生的问题, 解决思路如下所示
# 设置blog_index的mapping (版本2)
PUT blog_index
{
"mappings": {
"doc": {
"_source": {
"enabled": false
},
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 100
}
},
"store": true
},
"publish_date": {
"type": "date",
"store": true
},
"author": {
"type": "keyword",
"ignore_above": 100,
"store": true
},
"abstract": {
"type": "text",
"store": true
},
"content": {
"type": "text",
"store": true
},
"url": {
"type": "keyword",
"doc_values":false,
"norms":false,
"ignore_above": 100,
"store": true
}
}
}
}
}
# 为blog_index索引添加文档
POST blog_index/doc/1
{
"title": "blog title",
"content": "blog content"
}
# 检索blog_index索引的数据
# 此时可以利用stored_fields有条件的进行检索
GET blog_index/_search
{
"stored_fields": ["title"],
"query":{
"match": {
"content": "blog"
}
},
"highlight": {
"fields": {"content": {}}
}
}
# response
{
"took": 567,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.2876821,
"hits": [
{
"_index": "blog_index",
"_type": "doc",
"_id": "1",
"_score": 0.2876821,
"fields": {
"title": [
"blog title"
]
},
"highlight": {
"content": [
"blog content"
]
}
}
]
}
}
关联关系处理
- ES不擅长处理关系型数据库中的关联关系, 比如文章表blog与评论表comment之间通过blog_id关联, 在ES中可以通过如下两种手段变相解决
- Nested Object
- Parent/Child
- 评论表comment
- 文章Id blog_id
- 评论人 username
- 评论日期 date
- 评论内容 content
文章表与评论表在关系型数据库中的表示
关联关系处理之Nested Object
- ES中存储关联关系其中一个思路如下
- 但是其实这样存储会带来一些问题, 如下:
- 其原因是因为, Comment表默认存储为Object Array类型, 存储结构类似下面的形式:
- Nested Object可以解决这个问题
- 再次查询是将会得到正确的结果
- Nested Object Array的存储结构类似下面的形式
关联关系处理之Parent/Child
- ES还提供了类似于关系型数据库中join的实现方式, 使用join数据类型实现
- 使用join创建文档
常见query语法包括如下几种:
- parent_id返回某父文档的子文档
- has_child返回包含某子文档的父文档
- has_parent返回包含某父文档的子文档
Nested Object vs Parent/Child
Reindex
- 指重建所有数据的过程, 一般发生在如下情况:
- mapping 设置变更, 比如字段类型变化, 分词器字典更新等
- index设置变更, 比如分片数更改等
- 迁移数据
- ES提供了现成的API用于完成该工作
- _update_by_query在现有索引上重建
- _reindex在其他索引上重建
_update_by_query
_reindex
Task
数据重建的时间受索引文档规模的影响, 当规模越大时, 所需时间越多, 此时需要通过设定url参数 wait_for_completion为false来异步执行, ES以task来描述此类执行任务
ES提供了Task API来查看任务的执行进度和相关数据
其他建议
数据模型版本管理
- 对Mapping进行版本管理
- 包含在代码或者以专门文件进行管理, 添加好注释, 并加入GIt等版本管理仓库中, 方便回顾
- 为每一个增加metadata字段, 在其维护的一些文档相关的元数据, 方便对数据进行管理
防止字段过多
- 字段过多有如下坏处:
- 难以维护, 当字段成百上千时, 基本很难有人能明确知道每个字段的含义
- mapping的信息存储在cluster state里面, 过多的字段会导致mapping过大, 最终导致更新变慢
- 通过index.mapping.total_field.limit可以限定索引中最大字段数, 默认是1000
- 可以通过key/value的方式解决字段过多的问题, 但并不完美
key/value详解
- 虽然可以通过这种方式极大地减少field数目, 但也有一些明显的坏处
- query语句复杂度飙升, 且有一些可能无法实现, 比如聚合分析相关的
- 不利于在kibana中做可视化分析
- 一般字段过多的原因是由于没有高质量的数据建模导致的, 比如dynamic设置为了true
- 考虑拆分多个索引来解决问题